// Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.jasmin.unit.compiler;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public final class CircuitCodeGenerator {
  private static final int INDENT_SIZE = 2;
  
  private int indent;
  private Writer out;
  private boolean emptyLine = true;

  public void compile(CircuitDef circuit, List<Unit> unitDefs, Writer out) throws IOException {
    Map<String, Unit> unitDefsMap = new HashMap<String, Unit>();
    for (Unit unitDef : unitDefs) {
      unitDefsMap.put(unitDef.name, unitDef);
    }
    
    this.out = out;
    this.indent = 0;
    
    // Write package name
    if (circuit.packageName != null) {
      out("package " + circuit.packageName + ";");
    }
    newline();
    
    // Collect all import statements from all units (removing duplicates),
    // and write them all out.
    Set<String> imports = new TreeSet<String>();
    for (String anImport : circuit.imports) {
      imports.add(anImport);
    }
    for (Unit unit : unitDefs) {
      for (String anImport : unit.imports) {
        imports.add(anImport);
      }
    }
    for (String anImport : imports) {
      out("import " + anImport + ";");      
    }
    newline();    
    
    // Write class declaration opening
    out("public final class " + circuit.name + " {");
    outBegin();
    
    // Write public field declarations
    for (EndPoint node : circuit.nodes) {
      out("public " + node.type + " " + node.name + ";");
      if (node.tristate) {
        out("public " + node.type + " " + node.name+ "_Z;");
      }
    }
    newline();
    
    // Write unit field declarations
    for (NewUnitCall newUnit : circuit.units) {
      out("private final " + newUnit.qualifiedUnitName + " " + newUnit.fieldName + ";");
    }
    newline();
    
    // Write constructor opening
    outAppend("public " + circuit.name + "(");
    for (int i = 0; i < circuit.constructorArgs.argTypes.length; i++) {
      if (i != 0) outAppend(", ");
      outAppend(circuit.constructorArgs.argTypes[i] + " " + 
          circuit.constructorArgs.argNames[i]);
    }
    out(") {");
    outBegin();    
    for (NewUnitCall newUnitDecl : circuit.units) {
      out("// Initialize '" + newUnitDecl.fieldName + "' unit");
      out("{");
      outBegin();
      Unit unitDef = unitDefsMap.get(newUnitDecl.fieldName);
      String code = unitDef.constructorCode;
      String prefix = newUnitDecl.fieldName + "_";
      int i = 0;
      for (String constructorArgs : unitDef.constructorArgs) {
        code = replaceVariableNames(code, constructorArgs, newUnitDecl.unitArgs[i++]);
      }
      for (String fieldName : unitDef.fields) {
        code = replaceFieldNames(code, fieldName, prefix + fieldName);
      }
      outRaw(code);
      outEnd();
      out("}");
      newline();
    }
    outEnd();
    out("}");    
    newline();
    
    // Write update method 
    out("public void update() {");
    outBegin();
    for (String name : circuit.script) {
      out("// Updates '" + name + "' unit");
      Unit unitDef = unitDefsMap.get(name);
      String code = unitDef.updateMethodCode;
      String prefix = name + "_";
      for (String fieldName : unitDef.fields) {
        code = replaceFieldNames(code, fieldName, prefix + fieldName);
      }
      for (Map.Entry<String, String> entry : newNodeRenameMap(circuit, name).entrySet()) {
        String oldName = entry.getKey();
        String newName = entry.getValue();
        code = replaceFieldNames(code, oldName, newName);
      }
      out("{");
      outRaw(code);
      out("}");
    }
    
    outEnd();
    out("}");
    newline();
    
    // Close class declaration
    outEnd();
    out("}");
  }
  
  private void out(String line) throws IOException {
    if (emptyLine) for (int i = 0; i < indent * INDENT_SIZE; i++) out.write(" ");
    out.write(line);
    newline();
  }
  
  private void outRaw(String text) throws IOException {
    out.write(text);
  }
  
  private void outAppend(String text) throws IOException {
    if (emptyLine) for (int i = 0; i < indent * INDENT_SIZE; i++) out.write(" ");
    out.write(text);
    emptyLine = false;
  }
  
  private void newline() throws IOException {
    out.write("\n");
    emptyLine = true;
  }
  
  private void outBegin() {
    indent++;
  }
  
  private void outEnd() {
    indent--;
  }
  
  private String replaceVariableNames(String code, String oldName, String newName) {
    return replace(code, Regex.getVariableNamePattern(oldName), Regex.FIELD_NAME_GROUP, newName);
  }

  private String replaceFieldNames(String code, String oldName, String newName) {
    return replace(code, Regex.getFieldNamePattern(oldName), Regex.VARIABLE_NAME_GROUP, newName);
  }

  private String replaceMethodNames(String code, String oldName, String newName) {
    return replace(code, Regex.getMethodNamePattern(oldName), Regex.METHOD_NAME_GROUP, newName);
  }

  private String replace(String code, Pattern pattern, int group, String newText) {
    Matcher matcher = pattern.matcher(code);
    StringBuffer buffer = new StringBuffer();
    int index = 0;
    while (matcher.find()) {
      buffer.append(code.substring(index, matcher.start(group)));
      buffer.append(newText);
      index = matcher.end(group);
    }
    buffer.append(code.substring(index));
    return buffer.toString();
  }
  
  private Map<String, String> newNodeRenameMap(CircuitDef circuit, String name) {
    // Create node-rename mapping.
    Map<String, String> nodeRenameMap = new HashMap<String, String>();
    for (Jumper conn : circuit.jumpers) {
      if (conn.unit1 != null && conn.unit1.equals(name)) {
        if (conn.unit2 != null) {
          String newNodeName = conn.unit1 + "_" + conn.node1 + "__" + conn.unit2 + "_" + conn.node2;
          nodeRenameMap.put(conn.node1, newNodeName);
          nodeRenameMap.put("this." + conn.node1, newNodeName);
        } else {
          String newNodeName = conn.node2;
          nodeRenameMap.put(conn.node1, newNodeName);
          nodeRenameMap.put("this." + conn.node1, newNodeName);
        }
      } else if (conn.unit2 != null && conn.unit2.equals(name)) {
        if (conn.unit1 != null) {
          String newNodeName = conn.unit1 + "_" + conn.node1 + "__" + conn.unit2 + "_" + conn.node2;
          nodeRenameMap.put(conn.node2, newNodeName);
          nodeRenameMap.put("this." + conn.node2, newNodeName);
        } else {
          String newNodeName = conn.node1;
          nodeRenameMap.put(conn.node2, newNodeName);
          nodeRenameMap.put("this." + conn.node2, newNodeName);
        }
      }
    }
    return nodeRenameMap;
  }
}
